在處理資料的時候,我們常常會先把 Collection 裡的資料取出後,再逐一轉換成另外一個類別或格式,由於 Collection 有 Iterator 的特點,所以比較阿 Q 的作法就是用 2 個 for 迴圈完成。但其實 Kotlin 的標準函式庫裡內建了一系列的轉換 Extension 可以用,也就是說,這些 Extension 可以幫你將現有的 Collection 以傳入的 Lambda 做轉換,效率++!
map() 讓我們可以把 Collection 裡面的元素一個一個拿出來,然後套用一個 Lambda 讓它轉換成其他東西。就行為來說,map() 跟 forEach() 都會逐一碰過元素一遍,不過差異就在 map() 會把轉換的過程回傳而 forEach() 不會。另外一個特點就是,map() 回傳的 Collection 一定會和原本的 Collection 一樣大。
val numbers = listOf(1, 2, 3)
numbers.map { it * it } // [1, 4, 9]
假如取出元素時,想要一併取出 index 做處理,標準函式庫裡也有 mapIndexed() 可以使用,讓操作更彈性一些。
val numbers = listOf(1, 2, 3)
numbers.mapIndexed { index, value ->
"$index: $value"
}
// [0: 1, 1: 2, 2: 3]
假如 Lambda 操作結果有可能是 null 的話,我們可以用 mapNotNull() 或 mapIndexedNotNull() 來過濾,就不需自行再用 filterNotNull() 額外處理。
val listOfNumbers = listOf(1, 2, 3)
listOfNumbers.mapNotNull {
if ( it == 2) null else it * 3
} // [3, 9]
listOfNumbers.mapIndexedNotNull { index, value ->
if (index == 0) null else value * index
} // [2, 6]
當我們需要把兩個 Collection 的元素做成對照表時,可以用 zip(),它會將兩個 Collection 裡相同位置的元素做成 Pair 並回傳 List。你可以想像就是把兩個 Collection 像是用拉鏈一樣把兩邊黏起來一樣。
val colors = listOf("red", "brown", "grey")
val animals = listOf("fox", "bear", "wolf")
colors.zip(animals) // [(red, fox), (brown, bear), (grey, wolf)]
大多情況我們都會在兩個一樣大的 Collection 上套用 zip() 做 Pair。假如兩個 Collection 不一樣大的話,則會以小的為基準,多餘的都會被捨棄。
val animals = listOf("fox", "bear", "wolf")
val twoAnimals = listOf("fox", "bear")
colors.zip(twoAnimals) // [(red, fox), (brown, bear)]
twoAnimals.zip(colors) // [(fox, red), (bear, brown)]
有趣的是,zip() 有實作 infix function,所以你也可以把 zip 像關鍵字一樣地使用。
colors zip animals // [(red, fox), (brown, bear), (grey, wolf)]
zip() 還可以傳入 Lambda 做第二個參數,讓我們可以取得合併完後的每一個 Pair 內容做二次處理,這樣的彈性讓我們可以再少一步。
val colors = listOf("red", "brown", "grey")
val animals = listOf("fox", "bear", "wolf")
// 傳入 Lambda 做第二個參數,取得 Pair 裡的 color 和 animal
colors.zip(animals) { color, animal -> "The ${animal.capitalize()} is $color"}
// 回傳 [The Fox is red, The Bear is brown, The Wolf is grey]
// 同樣的結果用 `zip()` 和 `map()` 達成
colors.zip(animals).map {
"The ${it.second.capitalize()} is ${it.first}"
}
// 回傳 [The Fox is red, The Bear is brown, The Wolf is grey]
既然可以把兩個 Collection 黏在一起,當然就可以反過來拆開。假如你的 Collection 裡放了許多 Pair,就可以用 unzip() 把 key 和 value 各自拆成 List,然後再以 Pair 回傳。
val numberPairs = listOf("one" to 1, "two" to 2, "three" to 3, "four" to 4)
numberPairs.unzip() // ([one, two, three, four], [1, 2, 3, 4])
假如我們想要將 Collection 的元素與處理過的結果做對照,,可以用 associate 開頭的 method。它們可以將原本 Collection 的 key 或 value 跟 Lambda 處理過的結果做成 Map。
associateWith() 會將原本 Collection 裡的元素當成 key,把 Lambda 處理過的結果當成 value 組合成 Map。不過要注意的是,associateWith() 會把重覆的元素去掉,回傳的 Map 裡只留下最後一個不重覆的元素。
val numbers = listOf("one", "two", "two", "three", "four")
numbers.associateWith { it.length } // {one=3, two=3, three=5, four=4}
假如想要反過來,把原本 Collection 裡的元素當成 value,那就改用 associateBy()。或是你可以傳入 2 個 Lambda,keySelector 處理 key、valueTransform 處理 value。
val numbers = listOf("one", "two", "three", "four")
numbers.associateBy { it.first().toUpperCase() } // {O=one, T=three, F=four}
numbers.associateBy(
keySelector = { it.first().toUpperCase() },
valueTransform = { it.length }
) // {O=3, T=5, F=4}
associate() 的作法則是將 Collection 的元素傳給 Lambda,Lambda 則要回傳一個 Pair,最後會將這成群的 Pair 轉成 Map 回傳。
val names = listOf("Alice Adams", "Brian Brown", "Clara Campbell")
names.associate {
it.split(" ").let { (firstName, lastName) -> lastName to firstName }
}
// {Adams=Alice, Brown=Brian, Campbell=Clara}
假如你的 Collection 是巢狀的(比方說 List 裡面裝了數個 List),想要把 Collection 的階層打平,並取回各階層的元素成一個單層的 List 的話,flatten() 就是你要找的!
val numberSets = listOf(setOf(1, 2, 3), setOf(4, 5, 6), setOf(1, 2))
numberSets.flatten() // [1, 2, 3, 4, 5, 6, 1, 2]
flatMap() 則提供更彈性的巢狀 Collection 處理機制,它將傳入的 Lambda 套用在元素上以產生另一個 Collection,最後會再把階層打平回傳一個只有一層的 List。行為有點類似綜合運用了 flatten() 及 map()。
data class StringContainer(val values: List<String>)
val containers = listOf(
StringContainer(listOf("one", "two", "three")),
StringContainer(listOf("four", "five", "six")),
StringContainer(listOf("seven", "eight"))
)
containers.flatMap { it.values } // [one, two, three, four, five, six, seven, eight]
若需要將 Collection 的內容印出來方便閱讀,可以用 joinToString() 來合併成單一 String。可以注意到,joinToString() 跟 toString() 有點像但輸出的格式不同。
val numbers = listOf("one", "two", "three", "four")
numbers.joinToString() // one, two, three, four
numbers.toString() // [one, two, three, four]
若是要調整輸出的格式,joinToString() 可以傳入 separator、prefix、postfix 等參數,在輸出的時候,會先輸出 prefix,接著輸出每個元素並以 separator 隔開,最後再以 postfix 結束。
val numbers = listOf("one", "two", "three", "four")
numbers.joinToString(separator = " | ", prefix = "start: ", postfix = ": end") // start: one | two | three | four: end
假如 Collection 的內容很大,我們可能只需要印出前面幾個代表性的元素,後面就用「…」顯示。則可以 limit 設定輸出的個數、truncated 設定超過個數時要用什麼字串做結尾。因為 limit 和 truncated 是 joinToString() 的第 4-5 個參數,若前面的參數都不設定的話,記得要用顯示指定參數名稱。
val numbers = listOf("one", "two", "three", "four")
numbers.joinToString(limit = 2, truncated = "...") // one, two, ...
joinToString() 甚至還有第 6 個參數,你可以傳入一個 Lambda 決定要怎麼轉換成字串。
val numbers = listOf("one", "two", "three", "four")
numbers.joinToString { "Element: ${it.toUpperCase()}"} // Element: ONE, Element: TWO, Element: THREE, Element: FOUR
假如是要把轉換好的字串加到其他字串裡的話,則要改用 joinTo()。要傳入的參數記得要是一個 Appendable 物件。
val numbers = listOf("one", "two", "three", "four")
val listString = StringBuffer("The list of numbers: ")
numbers.joinTo(listString) // The list of numbers: one, two, three, four
在這個章節裡,我們實驗了許多 Collection 轉換的方式。熟悉這些 method 後,對於用 Kotlin 處理資料將會更加上手。許多 Kotlin 高手之所以程式碼可以如此簡潔,也是因為能將這些技巧發揮到極致的關係。為了一覽這些 API 在不同 Collection 上的行為,以下用表格來整理本章所討論到的 method:
| 行為 | Array | List | Set | Map | |
|---|---|---|---|---|---|
| map() | 轉換元素 | v | v | v | v |
| mapIndexed() | 轉換元素及索引 | v | v | v | v |
| mapNotNull() | 轉換元素後排除 null | v | v | v | v |
| mapIndexedNotNull() | 轉換元素及索引後排除 null | v | v | v | v |
| zip() | 將兩個集合相黏 | v | v | v | v |
| unzip() | 將 Pair 拆開 | v | v | v | v |
| flatten() | 將集合平面化 | v | v | v | v |
| flatMap() | 依 Lambda 把集合平面化 | v | v | v | v |
| joinToString() | 把元素合併成字串 | v | v | v | v |
| joinTo() | 把元素整併到別的字串 | v | v | v | v |